1use super::crypto::*;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::encoding::{decode_to_string, encode_string};
7use anyhow::Result;
8use std::collections::HashMap;
9use std::ffi::CString;
10use std::io::{Read, Seek, SeekFrom, Write};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
14pub struct EscudeBinArchiveBuilder {}
16
17impl EscudeBinArchiveBuilder {
18 pub const fn new() -> Self {
20 EscudeBinArchiveBuilder {}
21 }
22}
23
24impl ScriptBuilder for EscudeBinArchiveBuilder {
25 fn default_encoding(&self) -> Encoding {
26 Encoding::Cp932
27 }
28
29 fn default_archive_encoding(&self) -> Option<Encoding> {
30 Some(Encoding::Cp932)
31 }
32
33 fn build_script(
34 &self,
35 data: Vec<u8>,
36 _filename: &str,
37 _encoding: Encoding,
38 archive_encoding: Encoding,
39 config: &ExtraConfig,
40 _archive: Option<&Box<dyn Script>>,
41 ) -> Result<Box<dyn Script>> {
42 Ok(Box::new(EscudeBinArchive::new(
43 MemReader::new(data),
44 archive_encoding,
45 config,
46 )?))
47 }
48
49 fn build_script_from_file(
50 &self,
51 filename: &str,
52 _encoding: Encoding,
53 archive_encoding: Encoding,
54 config: &ExtraConfig,
55 _archive: Option<&Box<dyn Script>>,
56 ) -> Result<Box<dyn Script>> {
57 if filename == "-" {
58 let data = crate::utils::files::read_file(filename)?;
59 Ok(Box::new(EscudeBinArchive::new(
60 MemReader::new(data),
61 archive_encoding,
62 config,
63 )?))
64 } else {
65 let f = std::fs::File::open(filename)?;
66 let reader = std::io::BufReader::new(f);
67 Ok(Box::new(EscudeBinArchive::new(
68 reader,
69 archive_encoding,
70 config,
71 )?))
72 }
73 }
74
75 fn build_script_from_reader(
76 &self,
77 reader: Box<dyn ReadSeek>,
78 _filename: &str,
79 _encoding: Encoding,
80 archive_encoding: Encoding,
81 config: &ExtraConfig,
82 _archive: Option<&Box<dyn Script>>,
83 ) -> Result<Box<dyn Script>> {
84 Ok(Box::new(EscudeBinArchive::new(
85 reader,
86 archive_encoding,
87 config,
88 )?))
89 }
90
91 fn extensions(&self) -> &'static [&'static str] {
92 &["bin"]
93 }
94
95 fn script_type(&self) -> &'static ScriptType {
96 &ScriptType::EscudeArc
97 }
98
99 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
100 if buf_len > 8 && buf.starts_with(b"ESC-ARC2") {
101 return Some(255);
102 }
103 None
104 }
105
106 fn is_archive(&self) -> bool {
107 true
108 }
109
110 fn create_archive(
111 &self,
112 filename: &str,
113 files: &[&str],
114 encoding: Encoding,
115 config: &ExtraConfig,
116 ) -> Result<Box<dyn Archive>> {
117 let f = std::fs::File::create(filename)?;
118 let writer = std::io::BufWriter::new(f);
119 let archive = EscudeBinArchiveWriter::new(writer, files, encoding, config)?;
120 Ok(Box::new(archive))
121 }
122}
123
124#[derive(Debug)]
125struct BinEntry {
126 name_offset: u32,
127 data_offset: u32,
128 length: u32,
129}
130
131struct Entry {
132 name: String,
133 data: MemReader,
134}
135
136impl Read for Entry {
137 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
138 self.data.read(buf)
139 }
140}
141
142impl ArchiveContent for Entry {
143 fn name(&self) -> &str {
144 &self.name
145 }
146
147 fn script_type(&self) -> Option<&ScriptType> {
148 if self.data.data.starts_with(b"ESCR1_00") {
149 Some(&ScriptType::Escude)
150 } else if self.data.data.starts_with(b"LIST") {
151 Some(&ScriptType::EscudeList)
152 } else {
153 None
154 }
155 }
156
157 fn data(&mut self) -> Result<Vec<u8>> {
158 Ok(self.data.data.clone())
159 }
160
161 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
162 Ok(Box::new(&mut self.data))
163 }
164}
165
166#[derive(Debug)]
167pub struct EscudeBinArchive<T: Read + Seek + std::fmt::Debug> {
169 reader: Arc<Mutex<T>>,
170 file_count: u32,
171 entries: Vec<BinEntry>,
172 archive_encoding: Encoding,
173}
174
175impl<T: Read + Seek + std::fmt::Debug> EscudeBinArchive<T> {
176 pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
182 let mut header = [0u8; 8];
183 reader.read_exact(&mut header)?;
184 if &header != b"ESC-ARC2" {
185 return Err(anyhow::anyhow!("Invalid Escude binary script header"));
186 }
187 reader.seek(SeekFrom::Start(0xC))?;
188 let mut crypto_reader = CryptoReader::new(&mut reader)?;
189 let file_count = crypto_reader.read_u32()?;
190 let _name_tbl_len = crypto_reader.read_u32()?;
191 let mut entries = Vec::with_capacity(file_count as usize);
192 for _ in 0..file_count {
193 let name_offset = crypto_reader.read_u32()?;
194 let data_offset = crypto_reader.read_u32()?;
195 let length = crypto_reader.read_u32()?;
196 entries.push(BinEntry {
197 name_offset,
198 data_offset,
199 length,
200 });
201 }
202 Ok(EscudeBinArchive {
203 reader: Arc::new(Mutex::new(reader)),
204 file_count,
205 entries,
206 archive_encoding,
207 })
208 }
209}
210
211impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for EscudeBinArchive<T> {
212 fn default_output_script_type(&self) -> OutputScriptType {
213 OutputScriptType::Json
214 }
215
216 fn default_format_type(&self) -> FormatOptions {
217 FormatOptions::None
218 }
219
220 fn is_archive(&self) -> bool {
221 true
222 }
223
224 fn iter_archive_filename<'a>(
225 &'a self,
226 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
227 Ok(Box::new(EscudeBinArchiveIter {
228 entries: self.entries.iter(),
229 reader: self.reader.clone(),
230 file_count: self.file_count,
231 archive_encoding: self.archive_encoding,
232 }))
233 }
234
235 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
236 Ok(Box::new(
237 self.entries.iter().map(|e| Ok(e.data_offset as u64)),
238 ))
239 }
240
241 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
242 if index >= self.entries.len() {
243 return Err(anyhow::anyhow!(
244 "Index out of bounds: {} (max: {})",
245 index,
246 self.entries.len()
247 ));
248 }
249 let entry = &self.entries[index];
250 let name = self
251 .reader
252 .cpeek_cstring_at(entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14)?;
253 let name = decode_to_string(self.archive_encoding, name.as_bytes(), true)?;
254 let mut data = self
255 .reader
256 .cpeek_at_vec(entry.data_offset as u64, entry.length as usize)?;
257 if data.starts_with(b"acp") {
258 let mut decoder = match super::lzw::LZWDecoder::new(&data) {
259 Ok(decoder) => decoder,
260 Err(e) => return Err(anyhow::anyhow!("Failed to create LZW decoder: {}", e)),
261 };
262 data = decoder.unpack()?;
263 }
264 Ok(Box::new(Entry {
265 name,
266 data: MemReader::new(data),
267 }))
268 }
269}
270
271struct EscudeBinArchiveIter<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> {
272 entries: T,
273 reader: Arc<Mutex<R>>,
274 file_count: u32,
275 archive_encoding: Encoding,
276}
277
278impl<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> Iterator
279 for EscudeBinArchiveIter<'a, T, R>
280{
281 type Item = Result<String>;
282
283 fn next(&mut self) -> Option<Self::Item> {
284 let entry = match self.entries.next() {
285 Some(entry) => entry,
286 None => return None,
287 };
288 let name_offset = entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14;
289 let name = match self.reader.cpeek_cstring_at(name_offset) {
290 Ok(name) => name,
291 Err(e) => return Some(Err(e.into())),
292 };
293 let name = match decode_to_string(self.archive_encoding, name.as_bytes(), true) {
294 Ok(name) => name,
295 Err(e) => return Some(Err(e.into())),
296 };
297 Some(Ok(name))
298 }
299}
300
301pub struct EscudeBinArchiveWriter<T: Write + Seek> {
303 writer: T,
304 headers: HashMap<String, BinEntry>,
305 name_tbl_len: u32,
306 fake: bool,
307}
308
309impl<T: Write + Seek> EscudeBinArchiveWriter<T> {
310 pub fn new(
317 mut writer: T,
318 files: &[&str],
319 encoding: Encoding,
320 config: &ExtraConfig,
321 ) -> Result<Self> {
322 writer.write_all(b"ESC-ARC2")?;
323 let header_len = 0xC + 0xC * files.len();
324 let header = vec![0u8; header_len];
325 writer.write_all(&header)?;
326 let mut headers = HashMap::new();
327 for file in files {
328 let f = file.to_string();
329 let encoded = encode_string(encoding, file, false)?;
330 let encoded = CString::new(encoded)?;
331 let name_offset = writer.stream_position()? as u32;
332 writer.write_all(encoded.as_bytes_with_nul())?;
333 headers.insert(
334 f,
335 BinEntry {
336 name_offset,
337 data_offset: 0,
338 length: 0,
339 },
340 );
341 }
342 let name_tbl_len = writer.stream_position()? as u32 - header_len as u32 - 0x8;
343 Ok(EscudeBinArchiveWriter {
344 writer,
345 headers,
346 name_tbl_len,
347 fake: config.escude_fake_compress,
348 })
349 }
350}
351
352impl<T: Write + Seek> Archive for EscudeBinArchiveWriter<T> {
353 fn new_file<'a>(
354 &'a mut self,
355 name: &str,
356 _size: Option<u64>,
357 ) -> Result<Box<dyn WriteSeek + 'a>> {
358 let entry = self
359 .headers
360 .get_mut(name)
361 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
362 if entry.data_offset != 0 {
363 return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
364 }
365 entry.data_offset = self.writer.stream_position()? as u32;
366 Ok(Box::new(EscudeBinArchiveFileWithLzw::new(
367 entry,
368 &mut self.writer,
369 self.fake,
370 )?))
371 }
372
373 fn write_header(&mut self) -> Result<()> {
374 self.writer.seek(SeekFrom::Start(0x8))?;
375 let mut crypto = CryptoWriter::new(&mut self.writer)?;
376 let file_count = self.headers.len() as u32;
377 crypto.write_u32(file_count)?;
378 crypto.write_u32(self.name_tbl_len)?;
379 let mut entries: Vec<_> = self.headers.values().collect();
380 entries.sort_by(|a, b| a.name_offset.cmp(&b.name_offset));
381 for entry in entries {
382 let name_offset = entry.name_offset - file_count * 12 - 0x14;
383 crypto.write_u32(name_offset)?;
384 crypto.write_u32(entry.data_offset)?;
385 crypto.write_u32(entry.length)?;
386 }
387 Ok(())
388 }
389}
390
391pub struct EscudeBinArchiveFileWithLzw<'a, T: Write + Seek> {
393 writer: EscudeBinArchiveFile<'a, T>,
394 buf: MemWriter,
395 fake: bool,
396}
397
398impl<'a, T: Write + Seek> EscudeBinArchiveFileWithLzw<'a, T> {
399 fn new(header: &'a mut BinEntry, writer: &'a mut T, fake: bool) -> Result<Self> {
400 let writer = EscudeBinArchiveFile {
401 header,
402 writer,
403 pos: 0,
404 };
405 Ok(EscudeBinArchiveFileWithLzw {
406 writer,
407 buf: MemWriter::new(),
408 fake,
409 })
410 }
411}
412
413impl<'a, T: Write + Seek> Write for EscudeBinArchiveFileWithLzw<'a, T> {
414 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
415 self.buf.write(buf)
416 }
417
418 fn flush(&mut self) -> std::io::Result<()> {
419 self.buf.flush()
420 }
421}
422
423impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFileWithLzw<'a, T> {
424 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
425 self.buf.seek(pos)
426 }
427
428 fn stream_position(&mut self) -> std::io::Result<u64> {
429 self.buf.stream_position()
430 }
431
432 fn rewind(&mut self) -> std::io::Result<()> {
433 self.buf.rewind()
434 }
435}
436
437impl<'a, T: Write + Seek> Drop for EscudeBinArchiveFileWithLzw<'a, T> {
438 fn drop(&mut self) {
439 let buf = self.buf.as_slice();
440 let encoder = super::lzw::LZWEncoder::new();
441 let data = match encoder.encode(buf, self.fake) {
442 Ok(data) => data,
443 Err(e) => {
444 eprintln!("Failed to encode LZW data: {}", e);
445 crate::COUNTER.inc_error();
446 return;
447 }
448 };
449 match self.writer.write_all(&data) {
450 Ok(_) => {}
451 Err(e) => {
452 eprintln!("Failed to write LZW data: {}", e);
453 crate::COUNTER.inc_error();
454 }
455 }
456 }
457}
458
459pub struct EscudeBinArchiveFile<'a, T: Write + Seek> {
461 header: &'a mut BinEntry,
462 writer: &'a mut T,
463 pos: usize,
464}
465
466impl<'a, T: Write + Seek> Write for EscudeBinArchiveFile<'a, T> {
467 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
468 self.writer.seek(SeekFrom::Start(
469 self.header.data_offset as u64 + self.pos as u64,
470 ))?;
471 let written = self.writer.write(buf)?;
472 self.pos += written;
473 self.header.length = self.header.length.max(self.pos as u32);
474 Ok(written)
475 }
476
477 fn flush(&mut self) -> std::io::Result<()> {
478 self.writer.flush()
479 }
480}
481
482impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFile<'a, T> {
483 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
484 let new_pos = match pos {
485 SeekFrom::Start(offset) => offset as usize,
486 SeekFrom::End(offset) => {
487 if offset < 0 {
488 if (-offset) as usize > self.header.length as usize {
489 return Err(std::io::Error::new(
490 std::io::ErrorKind::InvalidInput,
491 "Seek from end exceeds file length",
492 ));
493 }
494 self.header.length as usize - (-offset) as usize
495 } else {
496 self.header.length as usize + offset as usize
497 }
498 }
499 SeekFrom::Current(offset) => {
500 if offset < 0 {
501 if (-offset) as usize > self.pos {
502 return Err(std::io::Error::new(
503 std::io::ErrorKind::InvalidInput,
504 "Seek from current exceeds current position",
505 ));
506 }
507 self.pos.saturating_sub((-offset) as usize)
508 } else {
509 self.pos + offset as usize
510 }
511 }
512 };
513 self.pos = new_pos;
514 Ok(self.pos as u64)
515 }
516}